﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Hvam.SharePoint.Search.Language;
using Hvam.SharePoint.Search.Language.Visitors;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Taxonomy;

namespace Hvam.SharePoint.Search.Query.Site
{
    internal class CamlExpressionVisitor : ExpressionVisitor
    {
        private static readonly Dictionary<ExpressionType, string> Operators =
            new Dictionary<ExpressionType, string>
                {
                    { ExpressionType.Equal,              "Eq"  },
                    { ExpressionType.NotEqual,           "Neq" },
                    { ExpressionType.LessThan,           "Lt"  },
                    { ExpressionType.LessThanOrEqual,    "Leq" },
                    { ExpressionType.GreaterThan,        "Gt"  },
                    { ExpressionType.GreaterThanOrEqual, "Geq" },
                };

        private StringBuilder CamlBuilder { get; set; }

        private Dictionary<string, SPField> Fields { get; set; }

        private SPSite Site { get; set; }

        public bool ExternalDataSource { get; set; }
        
        public string Translate(SPSite site, Dictionary<string, SPField> fields, Expression expression)
        {
            Site = site;
            Fields = fields;
            CamlBuilder = new StringBuilder();

            Visit(expression);

            return CamlBuilder.ToString();
        }

        protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
        {
            if (methodCallExpression.Method.Name == "Contains")
            {
                var property = (KeywordQueryProperty) ((ConstantExpression) methodCallExpression.Object).Value;
                var value = (ConstantExpression) methodCallExpression.Arguments.First();
                VisitRestriction("Contains", property, value);
            }

            return base.VisitMethodCall(methodCallExpression);
        }

        protected override Expression VisitBinary(BinaryExpression binaryExpression)
        {
            if (Operators.ContainsKey(binaryExpression.NodeType))
            {
                var property = (KeywordQueryProperty)((ConstantExpression)binaryExpression.Left).Value;
                var value = (ConstantExpression)binaryExpression.Right;

                VisitRestriction(Operators[binaryExpression.NodeType], property, value);

                return binaryExpression;
            }
            
            if (binaryExpression.NodeType == ExpressionType.And || 
                binaryExpression.NodeType == ExpressionType.AndAlso)
            {
                CamlBuilder.Append("<And>");

                var e = base.VisitBinary(binaryExpression);

                CamlBuilder.Append("</And>");
                return e;
            }

            if (binaryExpression.NodeType == ExpressionType.Or ||
                binaryExpression.NodeType == ExpressionType.OrElse)
            {
                CamlBuilder.Append("<Or>");

                var e = base.VisitBinary(binaryExpression);

                CamlBuilder.Append("</Or>");
                return e;
            }

            throw new CamlTranslationException(string.Format("Unexpected node type ('{0}')", binaryExpression.NodeType));
        }

        internal void VisitRestriction(string @operator, KeywordQueryProperty property, ConstantExpression value)
        {
            if (property.Name.StartsWith("owstaxId", StringComparison.OrdinalIgnoreCase))
            {
                VisitRestrictionTaxonomyRestriction(@operator, property, value);
                return;
            }

            if (!Fields.ContainsKey(property.Name))
            {
                throw new CamlTranslationException("Field not found.");
            }
            
            var field = Fields[property.Name];

            string fieldRef;
            if (ExternalDataSource)
            {
                fieldRef = string.Format("<FieldRef Name=\"{0}\"/>", field.InternalName);
            }
            else
            {
                fieldRef = string.Format("<FieldRef ID=\"{0}\"/>", field.Id);
            }

            if (@operator == "Eq" && value.Value.ToString() == string.Empty)
            {
                CamlBuilder.Append(string.Format("<Or>"));
                CamlBuilder.Append(string.Format("<IsNull>{0}</IsNull>", fieldRef));
            }

            if (@operator == "Neq" && value.Value.ToString() == string.Empty)
            {
                CamlBuilder.Append(string.Format("<And>"));
                CamlBuilder.Append(string.Format("<IsNotNull>{0}</IsNotNull>", fieldRef));
            }

            CamlBuilder.Append(string.Format("<{0}>", @operator));

            CamlBuilder.Append(fieldRef);

            switch (field.TypeAsString)
            {
                case "DateTime":
                    var dateTime = value.Value.ToString();
                    
                    var includeTimeValue = string.Empty;
                    if (dateTime.Length > 10)
                    {
                        includeTimeValue = " IncludeTimeValue=\"TRUE\"";
                    }
                        
                    CamlBuilder.Append(string.Format("<Value Type=\"{0}\"{2}>{1}</Value>", field.TypeAsString, dateTime, includeTimeValue));
                    break;
                default:
                    // TODO: normalize and encode value?
                    CamlBuilder.Append(string.Format("<Value Type=\"{0}\">{1}</Value>", field.TypeAsString, value.Value));
                    break;
            }

            CamlBuilder.Append(string.Format("</{0}>", @operator));

            if (@operator == "Eq" && value.Value.ToString() == string.Empty)
            {
                CamlBuilder.Append(string.Format("</Or>"));
            }

            if (@operator == "Neq" && value.Value.ToString() == string.Empty)
            {
                CamlBuilder.Append(string.Format("</And>"));
            }
        }

        private void VisitRestrictionTaxonomyRestriction(string @operator, KeywordQueryProperty property, ConstantExpression value)
        {
            // owstaxId
            var fieldName = property.Name.Substring(8);

            if (!Fields.ContainsKey(fieldName))
            {
                throw new CamlTranslationException("Field not found.");
            }
            var field = Fields[fieldName] as TaxonomyField;
            if (field == null)
            {
                throw new Exception("TaxonomyField expected.");
            }
            if (@operator != "Eq" && @operator != "Contains")
            {
                throw new CamlTranslationException("Expected Eq or Contains operator.");
            }
            var restriction = value.Value.ToString();
            if (!restriction.StartsWith("#"))
            {
                throw new CamlTranslationException("Expected taxonomy restriction to start with #.");
            }
            if (restriction.Length != 37 && restriction.Length != 38)
            {
                throw new CamlTranslationException("Invalid taxonomy restriction length.");
            }

            var includeDescendants = restriction.Length == 37;
            var termId = includeDescendants ? new Guid(restriction.Substring(1)) : new Guid(restriction.Substring(2));
            int[] lookupIds;
            
            if (field.IsKeyword)
            {
                lookupIds = TaxonomyField.GetWssIdsOfKeywordTerm(Site, termId, 500);
            }
            else
            {
                lookupIds = TaxonomyField.GetWssIdsOfTerm(Site, field.SspId, field.TermSetId, termId, includeDescendants, 500);
            }
            
            if (lookupIds.Length == 0)
            {
                // No wss ids found, return empty set
                CamlBuilder.Append("<Eq><FieldRef ID=\"1d22ea11-1e32-424e-89ab-9fedbadb6ce1\"/><Value Type=\"Counter\">0</Value></Eq>");
                return;
            }

            CamlBuilder.Append("<In>");
            CamlBuilder.Append(string.Format("<FieldRef LookupId=\"TRUE\" ID=\"{0}\"/>", field.Id));
            CamlBuilder.Append("<Values>");

            var values = lookupIds.ToList().ConvertAll(i => string.Format("<Value Type=\"Integer\">{0}</Value>", i));
            foreach (var v in values)
            {
                CamlBuilder.Append(v);
            }

            CamlBuilder.Append("</Values>");
            CamlBuilder.Append("</In>");
        }
    }
}
